/*
 * TxObjectProxy.java   0.0.1    30/ott/2009
 * 
 * Copyright (c) 2009 mentalsmash.org
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * The use of the Apache License does not indicate that this project is
 * affiliated with the Apache Software Foundation.
 */
package org.mentalsmash.tazio.transactions.model.impl;

import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Map;
import java.util.Set;

import org.mentalsmash.tazio.IllegalArgumentException;
import org.mentalsmash.tazio.transactions.control.ReadOnlyAccessException;
import org.mentalsmash.tazio.transactions.control.TazioTransaction;
import org.mentalsmash.tazio.transactions.control.TransactionStateException;
import org.mentalsmash.tazio.transactions.model.NoSuchPropertyException;
import org.mentalsmash.tazio.transactions.model.PrivateSnapshot;
import org.mentalsmash.tazio.transactions.model.Property;
import org.mentalsmash.tazio.transactions.model.PropertyUpdate;
import org.mentalsmash.tazio.transactions.model.Query;
import org.mentalsmash.tazio.transactions.model.RootSet;
import org.mentalsmash.tazio.transactions.model.TransactionalObject;
import org.mentalsmash.tazio.transactions.model.identifiers.OID;
import org.mentalsmash.tazio.transactions.user.RollbackException;
import org.mentalsmash.tazio.transactions.user.TransactionStatus;
import org.mentalsmash.tazio.transactions.user.TransactionalSystemException;
import org.mentalsmash.tazio.transactions.user.TransactionalSystemRuntimeException;
import org.mentalsmash.tazio.transactions.user.UserTransactionManager;
import org.mentalsmash.tazio.transactions.user.annotations.PropertyGetter;
import org.mentalsmash.tazio.transactions.user.annotations.PropertySetter;
import org.mentalsmash.tazio.transactions.user.annotations.Transactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.ImmutableList;

/**
 * The <tt>TxObjectProxy</tt> class has a twofold role. First it wraps tazio
 * transactional objects (i.e. objects implementing at least one interface
 * marked with the {@link Transactional} annotation, acting as a method
 * interceptor for the wrapped object. Second it implements the
 * {@link TransactionalObject} interface, adding its behaviuor to the wrapped
 * objects
 * 
 * @author Andrea Reale
 * @version 0.0.1
 */
public class TxObjectWrapper implements TransactionalObject, InvocationHandler {

    private static final long serialVersionUID = 89600307132209157L;

    private final static Logger log = LoggerFactory
	    .getLogger(TxObjectWrapper.class);

    private final OID objectId;
    private long version;

    private final Serializable targetObject;
    private final Class<?> transactionalInterface; // the interface annotated as
    // @Transactional

    private final Map<String, Property> properties;

    private final UserTransactionManager tm;
    private final RootSet rs;

    private final static Method[] transactionalObjectMethods = TransactionalObject.class
	    .getMethods();

    /**
     * Create a new <tt>TxObjectProxy</tt> for a given interface annotated with
     * the {@link Transactional} annotation.
     * 
     * @param objectId
     *            the OID to assign to the proxy
     * @param initialVersion
     *            the initial version to assign to the proxy
     * @param proxiedObject
     *            the transactional object instance implementing exactly one
     *            interface annotated with {@link Transactional}
     */
    protected TxObjectWrapper(final OID objectId, final long initialVersion,
	    final Serializable proxiedObject,
	    final Class<?> transactionalInterface,
	    final Map<String, Property> properties,
	    final UserTransactionManager tm, final RootSet rs) {

	this.transactionalInterface = transactionalInterface;
	this.targetObject = proxiedObject;
	this.objectId = objectId;
	this.version = initialVersion;

	this.properties = properties;
	this.tm = tm;
	this.rs = rs;

    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.mentalsmash.tazio.transactions.TransactionalObject#getObjectIdentifier
     * ()
     */
    @Override
    public OID getObjectIdentifier() {
	return this.objectId;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.mentalsmash.tazio.transactions.TransactionalObject#getPropertyNames()
     */
    @Override
    public String[] getPropertyNames() {
	final Set<String> propertyNames = this.properties.keySet();
	return propertyNames.toArray(new String[propertyNames.size()]);

    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.mentalsmash.tazio.transactions.TransactionalObject#getPropertyValue
     * (java.lang.String)
     */
    @Override
    public synchronized Serializable getPropertyValue(final String name)
	    throws NoSuchPropertyException {
	final Property prop = this.properties.get(name);

	if (prop == null) {
	    log.warn("Tried to access a non existing property on interface {}",
		    this.transactionalInterface.getSimpleName());
	    throw new NoSuchPropertyException("NoSuchProperty: " + name
		    + " for interface " + this.transactionalInterface);
	}

	try {
	    return (Serializable) prop.getter().invoke(this.targetObject);
	} catch (final java.lang.IllegalArgumentException e) {
	    log.error("UnexpectedException", e);
	    throw new TransactionalSystemRuntimeException(
		    "Unexpected Exception: ", e);
	} catch (final IllegalAccessException e) {
	    log.error("UnexpectedException", e);
	    throw new TransactionalSystemRuntimeException(
		    "Unexpected Exception: ", e);
	} catch (final InvocationTargetException e) {
	    log.error("UnexpectedException", e);
	    throw new TransactionalSystemRuntimeException(
		    "Unexpected Exception: ", e);
	}

    }

    /*
     * (non-Javadoc)
     * 
     * @see org.mentalsmash.tazio.transactions.TransactionalObject#getVersion()
     */
    @Override
    public synchronized long getVersion() {
	return this.version;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.mentalsmash.tazio.transactions.TransactionalObject#setPropertyValue
     * (java.lang.String, java.io.Serializable)
     */
    @Override
    public synchronized void setPropertyValue(final String name,
	    final Serializable value) throws NoSuchPropertyException,
	    IllegalArgumentException {

	final Property prop = this.properties.get(name);

	if (prop == null) {
	    log.warn("Tried to access a non existing property on interface {}",
		    this.transactionalInterface.getSimpleName());
	    throw new NoSuchPropertyException("NoSuchProperty: " + name
		    + " for interface " + this.transactionalInterface);
	}

	if (prop.isReadonly()) {
	    throw new ReadOnlyAccessException(
		    "Tried to set a read-only property");
	}

	// check the argument
	try {
	    prop.setter().invoke(this.targetObject, value);
	} catch (final java.lang.IllegalArgumentException e) {
	    log.error("Illegal type of parameter passed to setter method: {}",
		    value.getClass().getSimpleName());
	    throw new IllegalArgumentException(
		    "Illegal type of parameter passed to setter method: "
			    + value.getClass().getSimpleName());

	} catch (final IllegalAccessException e) {
	    log.error("UnexpectedException", e);
	    throw new TransactionalSystemRuntimeException(
		    "Unexpected Exception: ", e);
	} catch (final InvocationTargetException e) {
	    log.error("UnexpectedException", e);
	    throw new TransactionalSystemRuntimeException(
		    "Unexpected Exception: ", e);
	}

    }

    public Object invoke(final Object proxy, final Method method,
	    final Object[] args) throws Throwable {

	// 1. If a method of the interface TransactionalObject is called invoke
	// it on the wrapper.

	// 2. If a method of the @Transactional interface of the wrapped object
	// is called, intercept it, make sure it is called in a transaction and
	// invoke it on the wrapped object.

	// 3. For any other method, invoke it on the wrapped object

	Object invocationTarget = null;
	// the next tree variables are used to understand if a property of an
	// @Transactional interface was called, and in that case, to store the
	// proeprty name and whether a getter or a setter for that property is
	// being called
	boolean transactionalProperty = false;
	String propertyName = null;
	boolean isGetter = false;

	Object returnValue;

	for (final Method m2 : transactionalObjectMethods) {
	    if (method.equals(m2)) {
		invocationTarget = this;
		break;
	    }
	}

	// Nasty patch to properly handle invocations of equals and hascode

	if (invocationTarget == null
		&& (method.getName().equals("getWrapper") || ImmutableList.of(
			"equals", "hashCode").contains(method.getName()))) {
	    invocationTarget = this;
	}
	// END of nasty patch

	if (invocationTarget == null) {

	    // only if it is not being invoked a method of the
	    // TransactionalObject interface
	    if (method.isAnnotationPresent(PropertyGetter.class)) {
		final PropertyGetter getterAnnotation = method
			.getAnnotation(PropertyGetter.class);
		propertyName = getterAnnotation.propertyName();
		transactionalProperty = true;
		isGetter = true;

	    } else if (method.isAnnotationPresent(PropertySetter.class)) {
		final PropertySetter setterAnnotation = method
			.getAnnotation(PropertySetter.class);
		propertyName = setterAnnotation.propertyName();
		transactionalProperty = true;
		isGetter = false;
	    }

	    // in any case call the methods on the wrapped object.
	    invocationTarget = this.targetObject;
	}

	if (transactionalProperty) {
	    returnValue = invokeAndAssureTransaction(invocationTarget,
		    propertyName, isGetter, args);
	} else {
	    returnValue = invokeSimple(invocationTarget, method, args);
	}

	return returnValue;
    }

    private Object invokeAndAssureTransaction(final Object invocationTarget,
	    final String propertyName, final boolean isGetter,
	    final Object[] args) throws TransactionStateException,
	    RollbackException, TransactionalSystemException {

	boolean dedicatedTx = false;
	Object returnValue = null;

	TazioTransaction tx = (TazioTransaction) this.tm.getTransaction();
	PrivateSnapshot snap; // the snapshot of the transacitonal object
	// private to tx
	// check whether a transaction is running
	if (tx.getStatus().equals(TransactionStatus.NO_TRANSACTION)) {
	    log.trace("Transactional property of "
		    + "object {} was called, but no transaction "
		    + "was running." + " Startin a new transaction",
		    getObjectIdentifier());
	    tx = (TazioTransaction) this.tm.begin();
	    dedicatedTx = true;
	}

	if (!tx.getStatus().equals(TransactionStatus.RUNNING)) {
	    throw new TransactionStateException("Transaction must be RUNNING");
	}

	if (!this.properties.containsKey(propertyName)) {
	    log.error("Proxird Object {} did not contain a property"
		    + "named {}", this.targetObject, propertyName);
	    throw new TransactionalSystemRuntimeException("Proxied Object"
		    + this.targetObject + " did not contain a property named "
		    + propertyName);
	}

	log.debug("Intercepted access to a property {} on object ",
		propertyName, this.targetObject);

	// TODO: check whether this is not a SetSnapshot
	snap = (PrivateSnapshot) tx.retrieveSnapshot(this.objectId);

	if (snap == null) {
	    log.trace("Snapshotting object {} for transaction {}",
		    getObjectIdentifier(), tx.getTransactionId());
	    tx.takeSnapshot(this);
	    // TODO: check whether this is not a SetSnapshot
	    snap = (PrivateSnapshot) tx.retrieveSnapshot(this
		    .getObjectIdentifier());
	}

	if (isGetter) {
	    returnValue = getPropInTransaction(propertyName, tx, snap);
	} else {
	    setPropInTransaction((Serializable) args[0], propertyName, tx, snap);
	}

	if (dedicatedTx) {
	    tx.commit();
	}

	return returnValue;
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object,
     * java.lang.reflect.Method, java.lang.Object[])
     */
    // @Override
    // public Object invoke(Object objTarget, Method method, Object[] args)
    // throws Throwable {
    //
    // // arg0 is TxObjectProxy itself
    //
    // String propertyName = null;
    // boolean intercept = false;
    // boolean isGetter = false;
    // Object invocationTarget = null;
    // Object returnValue = null;
    //
    // // intercepting methods for the TransactionalObject interface
    // for (Method txMethod : TransactionalObject.class.getMethods()) {
    //
    // if (method.equals(txMethod)) {
    //
    // invocationTarget = this;
    // break;
    // }
    // }
    //
    // // Nasty patch to properly handle invocations of equals and hascode
    //
    // if (method.getName().equals("getWrapper")) {
    // invocationTarget = this;
    // }
    // if (ImmutableList.of("equals", "hashCode").contains(method.getName())) {
    // invocationTarget = this;
    // }
    // // END of nasty patch
    //
    // if (method.isAnnotationPresent(PropertyGetter.class)) {
    // PropertyGetter getterAnnotation = method
    // .getAnnotation(PropertyGetter.class);
    // propertyName = getterAnnotation.propertyName();
    // intercept = true;
    // isGetter = true;
    //
    // } else if (method.isAnnotationPresent(PropertySetter.class)) {
    // PropertySetter setterAnnotation = method
    // .getAnnotation(PropertySetter.class);
    // propertyName = setterAnnotation.propertyName();
    // intercept = true;
    // isGetter = false;
    // }
    //
    // // intercept is true if a property with a transactional annotation
    // // was called on the object. In such a case a transaction should
    // // be autmatically started if no transaction running
    // if (intercept) {
    // Transaction current = null;
    //
    // if (tm.getTransaction().getStatus().equals(
    // TransactionStatus.NO_TRANSACTION)) {
    // log.trace("Transactional property of "
    // + "object {} was called, but no transaction "
    // + "was running." + " Startin a new transaction",
    // getObjectIdentifier());
    // current = tm.begin();
    // }
    // returnValue = invokeInTransaction(args, propertyName, isGetter,
    // returnValue);
    // if (current != null) {
    // log.trace("Auto (async) commit of transaction {} started",
    // current.getTransactionId());
    // current.commitAsync();
    // }
    //
    // } else { // if nothing to intercept
    // returnValue = invokeWithoutTransaction(method, args,
    // invocationTarget, returnValue);
    // }
    //
    // return returnValue;
    // }

    /**
     * @param method
     * @param args
     * @param invocationTarget
     * @param returnValue
     * @return
     * @throws IllegalAccessException
     * @throws Throwable
     */
    private Object invokeSimple(final Object invocationTarget,
	    final Method method, final Object[] args)
	    throws IllegalAccessException, Throwable {

	Object returnValue = null;

	log.debug("Nothing to intercept");
	try {
	    returnValue = method.invoke(invocationTarget, args);
	} catch (final InvocationTargetException ex) {
	    throw ex.getCause();

	}
	return returnValue;
    }

    /**
     * @param args
     * @param propertyName
     * @param current
     * @param snap
     * @throws NoSuchPropertyException
     */
    private void setPropInTransaction(Serializable toWrite,
	    final String propertyName, final TazioTransaction current,
	    final PrivateSnapshot snap) throws NoSuchPropertyException {
	final PropertyUpdate writeOp = new PropertyUpdate(
		getObjectIdentifier(), snap.getInitialVersion());

	log.trace("Before invoking the setter");
	if (toWrite == null) {
	    toWrite = OID.NULL_OID;
	} else if (toWrite instanceof TransactionalObject) {
	    final OID id = ((TransactionalObject) toWrite)
		    .getObjectIdentifier();
	    log.trace("Property {} is to be set to the "
		    + "Transactional Object {}, writing its OID instead",
		    propertyName, id);
	    toWrite = id;
	}
	snap.updateProperty(propertyName, toWrite);
	writeOp.setPropertyName(propertyName);
	writeOp.setWrittenValue(toWrite);
	current.addToWriteSet(getObjectIdentifier());
	current.logOperation(writeOp);
	log.trace("After invoking the setter");
    }

    /**
     * @param propertyName
     * @param current
     * @param snap
     * @return
     * @throws NoSuchPropertyException
     */
    private Object getPropInTransaction(final String propertyName,
	    final TazioTransaction current, final PrivateSnapshot snap)
	    throws NoSuchPropertyException {

	Object returnValue;
	final Query readOp = new Query(getObjectIdentifier(), snap
		.getInitialVersion());
	log.trace("Before invoking the getter");
	returnValue = snap.readProperty(propertyName);
	readOp.setPropertyName(propertyName);
	readOp.setReadValue((Serializable) returnValue);

	if (returnValue instanceof OID) {

	    final OID id = (OID) returnValue;
	    // maybe it's just the null object
	    if (id.equals(OID.NULL_OID)) {
		log.trace("Property {} is set to the NULL_OID. Returning null");
		returnValue = null;
	    }

	    log.trace("Property {} is a transactional object. "
		    + "Retrieving object from root set", propertyName);
	    // retrieve the actual instance from the root set
	    returnValue = this.rs.getObject(id);

	    if (returnValue == null) {
		log.error("Unable to retrieve object {} from root set", id);
		throw new TransactionalSystemRuntimeException(
			"Unable to retrieve object" + id + " from the root set");
	    }
	}
	current.addToReadSet(getObjectIdentifier());
	current.logOperation(readOp);
	log.trace("After invoking the getter");
	return returnValue;
    }

    @Override
    public Class<?> getTransactionalInterface() {
	return this.transactionalInterface;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.mentalsmash.tazio.transactions.TransactionalObject#getPropertyType
     * (java.lang.String)
     */
    @Override
    public Class<?> getPropertyType(final String name)
	    throws NoSuchPropertyException {
	final Property prop = this.properties.get(name);

	if (prop == null) {
	    log.warn("Tried to access a non existing property on interface {}",
		    this.transactionalInterface.getSimpleName());
	    throw new NoSuchPropertyException("NoSuchProperty: " + name
		    + " for interface " + this.transactionalInterface);
	}

	return prop.getPropertyType();
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.mentalsmash.tazio.transactions.TransactionalObject#setVersion(long)
     */
    @Override
    public synchronized void setVersion(final long newVersion) {
	if (newVersion < 0) {
	    throw new IllegalArgumentException(
		    "Version number must be non-negative");
	}
	this.version = newVersion;

    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.mentalsmash.tazio.transactions.TransactionalObject#isReadOnlyProperty
     * (java.lang.String)
     */
    @Override
    public boolean isReadOnlyProperty(final String name)
	    throws NoSuchPropertyException {

	final Property prop = this.properties.get(name);
	if (prop == null) {
	    log.warn("Tried to access a non existing property on interface {}",
		    this.transactionalInterface.getSimpleName());
	    throw new NoSuchPropertyException("NoSuchProperty: " + name
		    + " for interface " + this.transactionalInterface);
	}

	return prop.isReadonly();
    }

    public TxObjectWrapper getWrapper() {
	return this;
    }

    @Override
    public int hashCode() {
	final int prime = 31;
	int result = 1;
	result = prime * result
		+ ((this.objectId == null) ? 0 : this.objectId.hashCode());
	result = prime
		* result
		+ ((this.targetObject == null) ? 0 : this.targetObject
			.hashCode());
	result = prime
		* result
		+ ((this.transactionalInterface == null) ? 0
			: this.transactionalInterface.hashCode());
	result = prime * result + (int) (this.version ^ (this.version >>> 32));
	return result;
    }

    @Override
    public boolean equals(Object obj) {
	if (this == obj) {
	    return true;
	}
	if (obj == null) {
	    return false;
	}

	if (Proxy.isProxyClass(obj.getClass())) {
	    // PATCH solving the problem of obj being a proxied instance
	    try {
		obj = invoke(obj,
			TxObjectWrapper.class.getMethod("getWrapper"),
			new Object[] {});
	    } catch (final SecurityException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	    } catch (final NoSuchMethodException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	    } catch (final Throwable e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	    }
	    // ENDPATCH
	}
	if (getClass() != obj.getClass()) {
	    return false;
	}
	final TxObjectWrapper other = (TxObjectWrapper) obj;
	if (this.objectId == null) {
	    if (other.objectId != null) {
		return false;
	    }
	} else if (!this.objectId.equals(other.objectId)) {
	    return false;
	}
	if (this.targetObject == null) {
	    if (other.targetObject != null) {
		return false;
	    }
	} else if (!this.targetObject.equals(other.targetObject)) {
	    return false;
	}
	if (this.transactionalInterface == null) {
	    if (other.transactionalInterface != null) {
		return false;
	    }
	} else if (!this.transactionalInterface
		.equals(other.transactionalInterface)) {
	    return false;
	}
	if (this.version != other.version) {
	    return false;
	}
	return true;
    }

}
